ThreadLocal 源码解析和使用场景
ThreadLocal 主要用途
ThreadLocal 是在 JDK 包里面提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作的自己本地内存里面的变量,从而避免了线程安全问题,创建一个ThreadLocal变量后每个线程会拷贝一个变量到自己本地内存,如下图:
- 从JAVA官方对 ThreadLocal 类的说明定义(定义在示例代码中):ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。
- 我们可以得知 ThreadLocal 的作用是:ThreadLocal 的作用是提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。
- 上述可以概述为:ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
ThreadLocal使用实例
使用例子来调试,看下ThreadLocal如何使用,从而加深理解。例子开启了两个线程,每个线程内部设置了本地变量的值,然后调用print函数打印当前本地变量的值,如果打印后调用了本地变量额remove方法则会删除本地内存中的该变量,代码如下:
|
|
运行结果:
|
|
- 代码(2)创建了一个ThreadLocal变量
- 代码(3)(4)分别创建了线程one和two
- 代码(5)启动了两个线程。
- 线程one中代码3.1通过set方法设置了localVariable的值,这个设置的其实是线程one本地内存中的一个拷贝,这个拷贝线程two是访问不了的。然后代码3.2调用了print函数,代码1.1通过get函数获取了当前线程(线程one)本地内存中localVariable的值。
- 线程two执行类似线程one
解开代码1.2的注释后,再次运行,运行结果为:
|
|
ThreadLocal实现原理
首先看下ThreadLocal相关的类的类图结构
如上类图可知 Thread 类中有一个 threadLocals 和 inheritableThreadLocals 都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 HashMap,默认每个线程中这个两个变量都为null。
只有当前线程第一次调用了 ThreadLocal 的set或者get方法时候才会进行创建。其实每个线程的本地变量不是存放到 ThreadLocal 实例里面的,而是存放到调用线程的 threadLocals 变量里面。
也就是说 ThreadLocal 类型的本地变量是存放到具体的线程内存空间的。
ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面存放起来,当调用线程调用它的 get 方法时候再从当前线程的 threadLocals 变量里面拿出来使用。
如果调用线程一直不终止那么这个本地变量会一直存放到调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时候可以通过调用ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。
另外 Thread 里面的threadLocals 为何设计为 map 结构那?很明显是因为每个线程里面可以关联多个 ThreadLocal 变量。
为什么使用了弱引用
ThreadLocalMap 中的存储实体 Entry 使用 ThreadLocal 作为 key,但这个 Entry 是继承弱引用 WeakReference 的,为什么要这样设计,使用了弱引用 WeakReference 会造成内存泄露问题吗?
- 首先,回答这个问题之前,我需要解释一下什么是强引用,什么是弱引用。
我们在正常情况下,普遍使用的是强引用:
|
|
当 a = null;b = null; 时,一段时间后,JAVA垃圾回收机制GC会将 a 和 b 对应所分配的内存空间给回收。
但考虑这样一种情况:
|
|
当 b 被设置成 null 时,那么是否意味这一段时间后GC工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即使 b 被设置成 null ,但 c 仍然持有对 b 的引用,而且还是强引用,所以GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就造成了 内存泄露。
那么我们该如何处理呢?
可以通过 c = null;,也可以使用弱引用 WeakReference w = new WeakReference(b); 。因为使用了弱引用 WeakReference,GC 是可以回收 b 原先所分配的空间的。
上述解释主要参考自:对ThreadLocal实现原理的一点思考
- 回到 ThreadLocal 的层面上,ThreadLocalMap 使用 ThreadLocal 的弱引用作为key,如果一个ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的get(), set(), remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为null 的 value。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析 ThreadLocal 使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
|
|
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
- key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用get() ,set(),remove()的时候会被清除。
- 比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key ,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 get() ,set(),remove()的时候会被清除。
因此, ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的 remove() 方法,清除数据。
在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。
上述解释主要参考自:深入分析 ThreadLocal 内存泄漏问题
常用操作的底层实现原理
根据上面的例子,我们进行调试,看看一下几个常用操作的实现原理。
get() 方法
|
|
调用 get() 操作获取 ThreadLocal 中对应当前线程存储的值时,进行了如下操作:
1 ) 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
2 ) 判断当前的 ThreadLocalMap 是否存在:
如果存在,则以当前的ThreadLocal为 key,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value 值,即为我们想要的当前线程对应此ThreadLocal的值,返回结果值。
如果不存在,则证明此线程没有维护的 ThreadLocalMap 对象,调用 setInitialValue 方法进行初始化。返回setInitialValue 初始化的值。
setInitialValue 方法的操作如下:
1 ) 调用initialValue获取初始化的值。
2 ) 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
3 ) 判断当前的ThreadLocalMap是否存在:
如果存在,则调用map.set 设置此实体entry。
如果不存在,则调用createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中。
set() 方法
|
|
调用 set(T value) 操作设置ThreadLocal中对应当前线程要存储的值时,进行了如下操作:
1 ) 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
2 ) 判断当前的 ThreadLocalMap 是否存在:
如果存在,则调用 map.set 设置此实体 entry。
如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中。
remove() 方法
|
|
调用 remove() 操作删除ThreadLocal中对应当前线程已存储的值时,进行了如下操作:
- 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
- 判断当前的 ThreadLocalMap 是否存在, 如果存在,则调用 map.remove ,以当前 ThreadLocal 为 key 删除对应的实体 entry。
ThreadLocalMap的内部底层实现
对 ThreadLocal 的常用操作实际是对线程Thread中的ThreadLocalMap进行操作,核心是ThreadLocalMap这个哈希表,接着我们来谈谈ThreadLocalMap的内部底层实现。
源代码:
|
|
ThreadLocalMap 的底层实现是一个定制的自定义 HashMap 哈希表,核心组成元素有:
1 ) Entry[] table; :底层哈希表 table, 必要时需要进行扩容,底层哈希表 table.length 长度必须是2的n次方。
2 ) int size;:实际存储键值对元素个数 entries
3 ) int threshold;:下一次扩容时的阈值,阈值 threshold = len 2 / 3 (底层哈希表table的长度)。当
size >= threshold
时,遍历 table 并删除 key 为 null 的元素,如果删除后`size >= threshold3/4`时,需要对table 进行扩容其中 Entry[] table; 哈希表存储的核心元素是 Entry ,Entry 包含:
1 ) ThreadLocal<?> k;:当前存储的 ThreadLocal 实例对象
2 ) Object value;:当前 ThreadLocal 对应储存的值value
需要注意的是,此 Entry 继承了弱引用 WeakReference ,所以在使用 ThreadLocalMap 时,发现
key == null
,则意味着此 key ThreadLocal 不在被引用,需要将其从 ThreadLocalMap 哈希表中移除。
|
|
ThreadLocalMap 的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的 ThreadLocal 的值时,才初始化创建一次(仅初始化一次)。初始化步骤如下:
1) 初始化底层数组 table 的初始容量为 16。
2) 获取 ThreadLocal 中的 threadLocalHashCode ,通过threadLocalHashCode & (INITIAL_CAPACITY - 1)
,即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length 的方式计算该实体的存储位置。
3) 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值
4)设置当前实际存储元素个数 size 为 1
5)设置阈值setThreshold(INITIAL_CAPACITY)
,为初始化容量 16 的 2/3。
源代码:
|
|
ThreadLocal 的 get() 操作实际是调用 ThreadLocalMap 的 getEntry(ThreadLocal<?> key) 方法,此方法快速适用于获取某一存在 key 的实体 entry,否则,应该调用
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法获取,这样做是为了最大限制地提高直接命中的性能,该方法进行了如下操作:1 ) 计算要获取的 entry 的存储位置,存储位置计算等价于:ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length。
2 ) 根据计算的存储位置,获取到对应的实体 Entry。判断对应实体 Entry 是否存在 并且 key 是否相等:
存在对应实体 Entry 并且对应 key 相等,即同一 ThreadLocal ,返回对应的实体 Entry。
不存在对应实体 Entry 或者 key 不相等,则通过调用
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法继续查找。getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法操作如下:1 ) 获取底层哈希表数组 table,循环遍历对应要查找的实体 Entry 所关联的位置。
2 ) 获取当前遍历的 entry 的 key ThreadLocal ,比较 key 是否一致,一致则返回。
3 ) 如果 key 不一致 并且 key 为 null,则证明引用已经不存在,这是因为 Entry 继承的是 WeakReference,这是弱引用带来的坑。调用
expungeStaleEntry(int staleSlot)
方法删除过期的实体 Entry(此方法不单独解释,请查看示例代码,有详细注释说明)。4 ) key不一致 ,key也不为空,则遍历下一个位置,继续查找。
5 ) 遍历完毕,仍然找不到则返回null。
源代码:
|
|
|
|
|
|
ThreadLocal 的set(T value)
操作实际是调用 ThreadLocalMap 的set(ThreadLocal<?> key, Object value)
方法,该方法进行了如下操作:
1 ) 获取对应的底层哈希表 table ,计算对应 threalocal 的存储位置。
2 ) 循环遍历 table 对应该位置的实体,查找对应的 threadLocal。
3 ) 获取当前位置的 threadLocal,如果 key threadLocal 一致,则证明找到对应的 threadLocal,将新值赋值给找到的当前实体 Entry 的 value 中,结束。
4 ) 如果当前位置的 key threadLocal 不一致,并且 key threadLocal 为 null,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)
方法(此方法不单独解释,请查看示例代码,有详细注释说明),替换该位置key == null
的实体为当前要设置的实体,结束。
5 ) 如果当前位置的 key threadLocal 不一致,并且 key threadLocal 不为 null ,则创建新的实体,并存放至当前位置 i tab[i] = new Entry(key, value);
,实际存储键值对元素个数size + 1
,由于弱引用带来了这个问题,所以要调用cleanSomeSlots(int i, int n)
方法清除无用数据(此方法不单独解释,请查看示例代码,有详细注释说明),才能判断现在的 size 有没有达到阀值 threshhold ,如果没有要清除的数据,存储元素个数仍然 大于 阈值 则调用 rehash 方法进行扩容(此方法不单独解释,请查看示例代码,有详细注释说明)。
|
|
ThreadLocal 的remove()
操作实际是调用 ThreadLocalMap 的remove(ThreadLocal<?> key)
方法,该方法进行了如下操作:
1 ) 获取对应的底层哈希表 table,计算对应 threalocal 的存储位置。
2 ) 循环遍历 table 对应该位置的实体,查找对应的 threadLocal。
3 ) 获取当前位置的threadLocal,如果 key threadLocal 一致,则证明找到对应的 threadLocal,执行删除操作,删除此位置的实体,结束。
ThreadLocal 在现时有什么应用场景
总的来说 ThreadLocal 主要是解决2种类型的问题:
- 解决并发问题:使用 ThreadLocal 代替 synchronized 来保证线程安全。同步机制采用了“以时间换空间”的方式,而ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
- 解决数据存储问题:ThreadLocal 为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。如一个Parameter对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用 ThreadLocal 解决。
应用场景:
Spring 使用 ThreadLocal 解决线程安全问题
- 我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域。就是因为 Spring 对一些 Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的 Bean 就可以在多线程中共享了。
- 一般的 Web 应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
示例代码:
|
|
ThreadLocal 和 synchronized 的区别
ThreadLocal 和 synchronized 关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。
- ThreadLocal是一个Java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
- Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
- 同步机制(synchronized关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
总结:
- ThreadLocal提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
- ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。
- 对ThreadLocal 的常用操作实际是对线程 Thread 中的 ThreadLocalMap 进行操作。
- ThreadLocalMap 的底层实现是一个定制的自定义哈希表,ThreadLocalMap 的阈值threshold = 底层哈希表 table 的长度
len * 2 / 3
,当实际存储元素个数 size 大于或等于 阈值 threshold 的3/4
时size >= threshold*3/4
,则对底层哈希表数组 table 进行扩容操作。 - ThreadLocalMap 中的哈希表 Entry[] table 存储的核心元素是 Entry ,存储的 key 是 ThreadLocal 实例对象,value 是ThreadLocal 对应储存的值value。需要注意的是,此Entry继承了弱引用 WeakReference,所以在使用ThreadLocalMap时,发现
key == null
,则意味着此key ThreadLocal不在被引用,需要将其从ThreadLocalMap哈希表中移除。 - ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收。所以,在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。如果我们不主动调用上述操作,则会导致内存泄露。
- 为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove() 来清理无用的Entry。这在操作在使用线程池时尤为重要。
- ThreadLocal 和synchronized的区别:同步机制(synchronized关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
- ThreadLocal主要是解决2种类型的问题:A. 解决并发问题:使用ThreadLocal代替同步机制解决并发问题。B. 解决数据存储问题:如一个Parameter对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用ThreadLocal解决。
- 每个线程内部都有一个名字为threadLocals的成员变量,该变量类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们set时候的值,每个线程的本地变量是存到到线程自己的内存变量threadLocals里面的,如果当前线程一直不消失那么这些本地变量会一直存到,所以可能会造成内存溢出,所以使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。如果子线程中想要使用父线程中的threadlocal变量该如何做那?敬请期待
Java中高并发编程必备基础之并发包源码剖析
一书出版